/*
 * Copyright 2002-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.aop.aspectj.annotation;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.AjType;
import org.aspectj.lang.reflect.AjTypeSystem;
import org.aspectj.lang.reflect.PerClauseKind;
import org.springframework.aop.Pointcut;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.aspectj.TypePatternClassFilter;
import org.springframework.aop.framework.AopConfigException;
import org.springframework.aop.support.ComposablePointcut;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

/**
 * Metadata for an AspectJ aspect class, with an additional Spring AOP pointcut
 * for the per clause.
 *
 * <p>Uses AspectJ 5 AJType reflection API, enabling us to work with different
 * AspectJ instantiation models such as "singleton", "pertarget" and "perthis".
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @see org.springframework.aop.aspectj.AspectJExpressionPointcut
 * @since 2.0
 */
@SuppressWarnings("serial")
public class AspectMetadata implements Serializable {

    /**
     * The name of this aspect as defined to Spring (the bean name) -
     * allows us to determine if two pieces of advice come from the
     * same aspect and hence their relative precedence.
     */
    private final String aspectName;

    /**
     * The aspect class, stored separately for re-resolution of the
     * corresponding AjType on deserialization.
     */
    private final Class<?> aspectClass;
    /**
     * Spring AOP pointcut corresponding to the per clause of the
     * aspect. Will be the Pointcut.TRUE canonical instance in the
     * case of a singleton, otherwise an AspectJExpressionPointcut.
     */
    private final Pointcut perClausePointcut;
    /**
     * AspectJ reflection information (AspectJ 5 / Java 5 specific).
     * Re-resolved on deserialization since it isn't serializable itself.
     */
    private transient AjType<?> ajType;


    /**
     * Create a new AspectMetadata instance for the given aspect class.
     *
     * @param aspectClass the aspect class
     * @param aspectName  the name of the aspect
     */
    public AspectMetadata(Class<?> aspectClass, String aspectName) {
        this.aspectName = aspectName;

        Class<?> currClass = aspectClass;
        AjType<?> ajType = null;
        while (currClass != Object.class) {
            AjType<?> ajTypeToCheck = AjTypeSystem.getAjType(currClass);
            if (ajTypeToCheck.isAspect()) {
                ajType = ajTypeToCheck;
                break;
            }
            currClass = currClass.getSuperclass();
        }
        if (ajType == null) {
            throw new IllegalArgumentException("Class '" + aspectClass.getName() + "' is not an @AspectJ aspect");
        }
        if (ajType.getDeclarePrecedence().length > 0) {
            throw new IllegalArgumentException("DeclarePrecendence not presently supported in Spring AOP");
        }
        this.aspectClass = ajType.getJavaClass();
        this.ajType = ajType;

        switch (this.ajType.getPerClause().getKind()) {
            case SINGLETON:
                this.perClausePointcut = Pointcut.TRUE;
                return;
            case PERTARGET:
            case PERTHIS:
                AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut();
                ajexp.setLocation(aspectClass.getName());
                ajexp.setExpression(findPerClause(aspectClass));
                ajexp.setPointcutDeclarationScope(aspectClass);
                this.perClausePointcut = ajexp;
                return;
            case PERTYPEWITHIN:
                // Works with a type pattern
                this.perClausePointcut = new ComposablePointcut(new TypePatternClassFilter(findPerClause(aspectClass)));
                return;
            default:
                throw new AopConfigException(
                        "PerClause " + ajType.getPerClause().getKind() + " not supported by Spring AOP for " + aspectClass);
        }
    }

    /**
     * Extract contents from String of form {@code pertarget(contents)}.
     */
    private String findPerClause(Class<?> aspectClass) {
        String str = aspectClass.getAnnotation(Aspect.class).value();
        str = str.substring(str.indexOf('(') + 1);
        str = str.substring(0, str.length() - 1);
        return str;
    }


    /**
     * Return AspectJ reflection information.
     */
    public AjType<?> getAjType() {
        return this.ajType;
    }

    /**
     * Return the aspect class.
     */
    public Class<?> getAspectClass() {
        return this.aspectClass;
    }

    /**
     * Return the aspect name.
     */
    public String getAspectName() {
        return this.aspectName;
    }

    /**
     * Return a Spring pointcut expression for a singleton aspect.
     * (e.g. {@code Pointcut.TRUE} if it's a singleton).
     */
    public Pointcut getPerClausePointcut() {
        return this.perClausePointcut;
    }

    /**
     * Return whether the aspect is defined as "perthis" or "pertarget".
     */
    public boolean isPerThisOrPerTarget() {
        PerClauseKind kind = getAjType().getPerClause().getKind();
        return (kind == PerClauseKind.PERTARGET || kind == PerClauseKind.PERTHIS);
    }

    /**
     * Return whether the aspect is defined as "pertypewithin".
     */
    public boolean isPerTypeWithin() {
        PerClauseKind kind = getAjType().getPerClause().getKind();
        return (kind == PerClauseKind.PERTYPEWITHIN);
    }

    /**
     * Return whether the aspect needs to be lazily instantiated.
     */
    public boolean isLazilyInstantiated() {
        return (isPerThisOrPerTarget() || isPerTypeWithin());
    }


    private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
        inputStream.defaultReadObject();
        this.ajType = AjTypeSystem.getAjType(this.aspectClass);
    }

}
